package mbsl.commons.base;

import java.util.ArrayList;
import java.util.List;

import mbsl.commons.lib.BasicElement;
import mbsl.storage.Database;
import mbsl.ui.Combo;

/**
 * A class intended to make working with BasicElements a little easier.
 * <p>
 * The idea is to have one instance per BasicElement you need to work with, and delegate the work. For example if you
 * have a {@link Combo} which is tied to the class {@code T}, you can register it here, and it will keep it updated
 * for you, when adding new {@code T}s.
 * @param <T> A BasicElement-extending Class.
 */
public class BasicElementController<T extends BasicElement> {
	public BasicElementController(Database pDatabase, Class<T> pBasicElement) {
		mListArguments = "order by lower(name)";
		mSearchArguments = "where lower(name) = lower('%s')";
		mDatabase = pDatabase;
		mClass = pBasicElement;
	}
	/**
	 * Returns a database listing of {@code T} in the order specified by {@link #setListArguments(String)}.
	 */
	public List<T> getElementList() {
		return mDatabase.getList(mClass, getListArguments());
	}
	/**
	 * Returns a database listing of {@code T} in the order specified by the argument.
	 * @param pArguments
	 * @return
	 */
	public List<T> getElementList(String pArguments) {
		return mDatabase.getList(mClass, pArguments);
	}
	/**
	 * Saves the element, and adds it to the registered Combos.
	 */
	public void save(T pElement) {
		mDatabase.save(pElement);
		
		updateCombos();
	}
	/**
	 * Attempts to convert an object into {@code T}.  Useful for example with Combo boxes.
	 * <p>
	 * The procedure is as follows:
	 * <ul>
	 * <li>1. Check if {@code object} is {@code null}.  If it is, return {@code null}.
	 * <li>2. Check if {@code object} is of {@code T}.  If so, return it as a {@code T}.
	 * <li>3. Check if {@code object} is a {@link String}.  If not, throw an {@link IllegalArgumentException}.
	 * <li>4. Check if the String is empty.  If it is, return {@code null}.
	 * <li>5. Check if an object of {@code T} with the String as parameters exists in the database.  If so, retrieve and return it.
	 * <li>6. Create a new object of {@code T} with the String as parameters and return it.
	 * </ul>
	 * @param pObject The object to convert.
	 * @param pSave Whether to save the new element before it's returned. If this is false, the element won't be added to the registered Combos.
	 * @throws IllegalArgumentException Cast if the object is not an instance of {@code T}, String, or {@code null}, or if {@code T} is abstract or non-public.
	 * @return The converted object, or {@code null} if the object passed is {@code null} itself, or an empty String.
	 */
	public T getBasicElement(Object pObject, boolean pSave) {
		// If it's null, just return null
		if (pObject == null)
			return null;
		// If not, determine whether what we have is an object of this kind already.
		if (mClass.isInstance(pObject))
			// If it is, just cast and return it.
			return mClass.cast(pObject);
		else if (pObject != null && !(pObject instanceof String))
			throw new IllegalArgumentException(String.format("Object must be of T, a String, or null.  (T is %1$s and object is %2$s)", mClass.getCanonicalName(), pObject.getClass().getCanonicalName()));
		
		// Now we know it's a String.
		String string = ((String)pObject).trim();

		if (string.isEmpty())
			// In this case it's nothing we can work with, so
			return null;
		
		// Now we know it's a non-empty string, so let's check if there is an object by that name in the database already
		List<T> elementList = mDatabase.getList(mClass, getSearchQuery(string));
		
		if (elementList.size() > 0) // We found one, so cast and return
			return elementList.get(0);
		
		// Now we know it's an element we've never seen before, so let's create an object for it.
		T element;
		
		try {
			// Create a new object of the provided class.
			element = mClass.newInstance();
		} catch (Exception e) {
			// This could happen if pClass is abstract or non-public
			throw new IllegalArgumentException(e.getMessage());
		}
		
		// Give it its name from the String.
		fillData(element, string);
		
		if (pSave)
			save(element);
		
		// Cast it to the provided class, and return it.
		return element;
	}
	/** 
	 * Equal to getBasicElement(object, true)
	 * @see #getBasicElement(Object, boolean)
	 */
	public T getBasicElement(Object pObject) {
		return getBasicElement(pObject, true);
	}

	/** 
	 * Sets the arguments used when asking the Database for a list of this element.
	 * <p>
	 * By default, this is "order by lower(name)"
	 */
	public void setListArguments(String query) {
		mListArguments = query;
	}
	public String getListArguments() {
		return mListArguments;
	}
	/**
	 * Sets the arguments used when asking to look for a specific element.
	 * <p>
	 * By default, this is "where lower(name) = lower('%s')", where %s is what the parameter sent to {@link #getBasicElement(Object)}
	 * contains, if it's a String. If you need more control over how the query is created (for example if you need to parse the input
	 * somehow), override {@link #getSearchQuery(String)}.
	 * @param searchArguments The arguments sent to Hibernate when asking it to look for a specific element.  Must be valid HQL.
	 */
	public void setSearchArguments(String searchArguments) {
		mSearchArguments = searchArguments;
	}
	public String getSearchArguments() {
		return mSearchArguments;
	}
	/**
	 * Returns the formatted query String, ready to be sent to Hibernate.
	 * <p>
	 * This method is mostly here so that users can override it, and insert more complex argument Strings for elements that needs it.
	 * If for example an element is unique across two or more columns, the default {@link #setSearchArguments(String)} won't do.
	 * @param pData The String sent to {@link #getBasicElement(Object)}.
	 * @see #fillData(BasicElement, String)
	 */
	protected String getSearchQuery(String pData) {
		return String.format(getSearchArguments(), pData);
	}
	/**
	 * Fills the data from the String passed to {@link #getBasicElement(Object)}, into the empty element.
	 * <p>
	 * By default, this does: {@code element.setName(data)}
	 * <p>
	 * If the String contains multiple pieces of data, and needs to be parsed, override this method.
	 * @param pElement The empty T to fill.
	 * @param pData The data used to fill it.
	 */
	protected void fillData(T pElement, String pData) {
		pElement.setName(pData);
	}
	
	private void updateCombos() {
		if (mCombos == null)
			return;
		
		List<T> freshList = getElementList();
		
		for (Combo combo : mCombos) {
			/* As in the ElementControllers, I'd love to do this more efficiently, but in order to account for - well - ordering
			 * (which can be something other than the default), we need to have Hibernate generate the list for us.
			 */
			combo.clear();
			
			combo.add(freshList);
		}
	}
	/**
	 * Adds a Combo to this controller, which it will populate and update as needed.
	 * @param pCombo
	 */
	public void addCombo(Combo pCombo) {
		if (mCombos == null)
			mCombos = new ArrayList<Combo>();
		
		mCombos.add(pCombo);
		
		pCombo.clear();
			
		pCombo.add(getElementList());
	}
	/** Removes the passed Combo from the controller */
	public void removeCombo(Combo pCombo) {
		mCombos.remove(pCombo);
	}
	
	private List<Combo> mCombos;
	private Class<T> mClass;
	private String mListArguments;
	private String mSearchArguments;
	private Database mDatabase;
}
